TypeScript 类、接口 基本用法
类 Class
class class_name {
// 类作用域
}
定义类的关键字为 class
,后面紧跟类名,类可以包含以下几个模块(类的数据成员):
- 字段 − 字段是类里面声明的变量。字段表示对象的有关数据。
- 构造函数 − 类实例化时调用,可以为类的对象分配内存。
- 方法 − 方法为对象要执行的操作。
创建类
class Car {
// 字段
engine:string;
// 构造函数
constructor(engine:string) {
this.engine = engine
}
// 方法
disp():void {
console.log("发动机为 : "+this.engine)
}
}
? 和 ! 符号的作用
参考资料 巧用ES系列4: TypeScript中的问号 ? 与感叹号 ! 是什么意思?
什么是 ?(问号)操作符?
三元运算符
// 当 isNumber(input) 为 True 是返回 ? : 之间的部分; isNumber(input) 为 False 时
// 返回 : ; 之间的部分
const a = isNumber(input) ? input : String(input);
参数
// 这里的 ?表示这个参数 field 是一个可选参数
function getUser(user: string, field?: string) {
}
成员
// 这里的?表示这个name属性有可能不存在
class A {
name?: string
}
interface B {
name?: string
}
安全链式调用
// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
return new Error().stack.split('\n');
// 我们可以添加?操作符,当stack属性存在时,调用 stack.split。若stack不存在,则返回空
return new Error().stack?.split('\n');
// 以上代码等同以下代码
const err = new Error();
return err.stack && err.stack.split('\n');
什么是!(感叹号)操作符?
一元运算符
// ! 就是将之后的结果取反,比如:
// 当 isNumber(input) 为 True 时返回 False; isNumber(input) 为 False 时返回True
const a = !isNumber(input);
成员
// 因为接口B里面name被定义为可空的值,但是实际情况是不为空的,那么我们就可以
// 通过在class里面使用!,重新强调了name这个不为空值
class A implemented B {
name!: string
}
interface B {
name?: string
}
强制链式调用
// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
new Error().stack.split('\n');
// 我们确信这个字段100%出现,那么就可以添加!,强调这个字段一定存在
new Error().stack!.split('\n');
类的继承
class Shape {
Area:number
constructor(a:number) {
this.Area = a
}
}
class Circle extends Shape {
disp():void {
console.log("圆的面积: "+this.Area)
}
}
var obj = new Circle(223);
obj.disp()
继承类的方法重写
class PrinterClass {
doPrint():void {
console.log("父类的 doPrint() 方法。")
}
}
class StringPrinter extends PrinterClass {
doPrint():void {
super.doPrint() // 调用父类的函数
console.log("子类的 doPrint()方法。")
}
}
static 关键字
class StaticMem {
static num:number;
static disp():void {
console.log("num 值为 "+ StaticMem.num)
}
}
StaticMem.num = 12 // 初始化静态变量
StaticMem.disp() // 调用静态方法
instanceof 运算符
instanceof
运算符用于判断对象是否是指定的类型,如果是返回 true,否则返回 false。
class Person{ }
var obj = new Person()
var isPerson = obj instanceof Person;
console.log("obj 对象是 Person 类实例化来的吗? " + isPerson);
访问控制修饰符
TypeScript 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。TypeScript 支持 3 种不同的访问权限。
- public(默认) : 公有,可以在任何地方被访问。
- protected : 受保护,可以被其自身以及其子类和父类访问。
- private : 私有,只能被其定义所在的类访问。
class Encapsulate {
str1:string = "hello"
private str2:string = "world"
}
var obj = new Encapsulate()
console.log(obj.str1) // 可访问
console.log(obj.str2) // 编译错误, str2 是私有的
类和接口
interface ILoan {
interest:number
}
class AgriLoan implements ILoan {
interest:number
rebate:number
constructor(interest:number,rebate:number) {
this.interest = interest
this.rebate = rebate
}
}
var obj = new AgriLoan(10,1)
console.log("利润为 : "+obj.interest+",抽成为 : "+obj.rebate )
接口 Interfaces
// 对象的结构化类型
let persion: {
name: string;
age: number;
}
// 这样赋值就会被上面的结构所约束
persion = {
name: '张三',
age: 18
}
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
interface IPerson {
firstName:string,
lastName:string,
sayHi: ()=>string
}
let customer:IPerson = {
firstName:"Tom",
lastName:"Hanks",
sayHi: ():string =>{return "Hi there"}
}
接口初探
下面通过一个简单示例来观察接口是如何工作的:
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
类型检查器会查看 printLabel
的调用。 printLabel
有一个参数,并要求这个对象参数有一个名为 label
类型为 string
的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。
下面我们重写上面的例子,这次使用接口来描述:必须包含一个 label
属性且类型为 string
:
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
LabelledValue
接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label
属性且类型为 string
的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel
的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。
还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以
可选属性
有时我们希望不要完全匹配一个形状,那么可以用可选属性:
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ?
符号。
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom'
};
let tom: Person = {
name: 'Tom',
age: 25
};
只读属性
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly
定义只读属性:
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
函数类型
interface SearchFunc {
age: number;
subF(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = {
age: 18,
subF : function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
}
如果里面只有一个函数可以这样写:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
可索引的类型
与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如 a[10]
或 ageMap["daniel"]
。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上面例子里,我们定义了 StringArray
接口,它具有索引签名。 这个索引签名表示了当用 number
去索引 StringArray
时会得到 string
类型的返回值。
实现接口
与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
也可以在接口中描述一个方法,在类里实现它,如同下面的 setTime
方法一样:
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
Type 类型
参考资料 typescript 中的 interface 和 type 到底有什么区别?
interface 是接口,type 是类型,本身是两个概念。只是碰巧表现上比较相似。希望定义一个变量类型,就用 type,如果希望是能够继承并约束的,就用 interface。如果你不知道该用哪个,说明你只是想定义一个类型而非接口,所以应该用 type。
interface User {
name: string
age: number
}
interface SetUser {
(name: string, age: number): void;
}
type User = {
name: string
age: number
};
type SetUser = (name: string, age: number): void;
抽象类 abstract class
abstract class Animal{
public name:string;
constructor(name:string){
this.name=name;
}
//抽象方法 ,不包含具体实现,要求子类中必须实现此方法
abstract eat():any;
//非抽象方法,无需要求子类实现、重写
run(){
console.log('非抽象方法,不要子类实现、重写');
}
}
class Cat extends Animal{
//子类中必须实现父类抽象方法,否则ts编译报错
eat(){
return this.name+"吃鱼";
}
}
TypeScript 命名空间
namespace
是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。
由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module
关键字表示内部模块。但由于后来 ES6 也使用了 module
关键字,ts 为了兼容 ES6,使用 namespace
替代了自己的 module
,更名为命名空间。
随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace
,而推荐使用 ES6 的模块化方案了,故我们不再需要学习 namespace
的使用了。
namespace
被淘汰了,但是在声明文件中,declare namespace
还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。
命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的。这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中。
namespace SomeNameSpaceName {
export interface ISomeInterfaceName { }
export class SomeClassName { }
}
以上定义了一个命名空间 SomeNameSpaceName,如果我们需要在外部可以调用 SomeNameSpaceName 中的类和接口,则需要在类和接口添加 export
关键字。
要在另外一个命名空间调用语法格式为:
SomeNameSpaceName.SomeClassName;
如果一个命名空间在一个单独的 TypeScript 文件中,则应使用三斜杠 /// 引用它,语法格式如下:
/// <reference path = "SomeFileName.ts" />
使用命名空间
以下实例演示了命名空间的使用,定义在不同文件中:
IShape.ts
文件代码:
namespace Drawing {
export interface IShape {
draw();
}
}
Circle.ts
文件代码:
/// <reference path = "IShape.ts" />
namespace Drawing {
export class Circle implements IShape {
public draw() {
console.log("Circle is drawn");
}
}
}
Triangle.ts
文件代码:
/// <reference path = "IShape.ts" />
namespace Drawing {
export class Triangle implements IShape {
public draw() {
console.log("Triangle is drawn");
}
}
}
TestShape.ts
文件代码:
/// <reference path = "IShape.ts" />
/// <reference path = "Circle.ts" />
/// <reference path = "Triangle.ts" />
function drawAllShapes(shape:Drawing.IShape) {
shape.draw();
}
drawAllShapes(new Drawing.Circle());
drawAllShapes(new Drawing.Triangle());
使用 tsc 命令编译以上代码:
tsc --out app.js TestShape.ts
使用 node 命令查看输出结果为:
node app.js
-> Circle is drawn
-> Triangle is drawn
嵌套命名空间
命名空间支持嵌套,即你可以将命名空间定义在另外一个命名空间里头。
namespace namespace_name1 {
export namespace namespace_name2 {
export class class_name { }
}
}
成员的访问使用点号 .
来实现,如下实例:
Invoice.ts
文件代码:
namespace Runoob {
export namespace invoiceApp {
export class Invoice {
public calculateDiscount(price: number) {
return price * .40;
}
}
}
}
InvoiceTest.ts
文件代码:
/// <reference path = "Invoice.ts" />
var invoice = new Runoob.invoiceApp.Invoice();
console.log(invoice.calculateDiscount(500));
使用 tsc 命令编译以上代码:
tsc --out app.js InvoiceTest.ts